// Copyright 2007 Blue Onion Software
// All rights reserved

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Runtime.InteropServices;
using System.Security;
using System.Security.Permissions;
using System.Xml.Serialization;
using BlueOnionSoftware.Bloget.Properties;
using Microsoft.Win32;
using FILETIME=System.Runtime.InteropServices.ComTypes.FILETIME;

namespace BlueOnionSoftware.Bloget
{
    /// <summary>
    /// Information about a time zone
    /// </summary>
    public class TimeZoneInformation : IComparable<TimeZoneInformation>
    {
        TZI tzi;
        int index;
        string name = string.Empty;
        string displayName = string.Empty;
        string standardName = string.Empty;
        string daylightName = string.Empty;

        static TimeZoneInformation[] timeZones;
        static readonly object lockTimeZones = new object();
        static bool timezoneSecurityExceptionOccured;

        internal static DateTime ToLocalTime(int index, DateTime dateTime)
        {
            if (dateTime.Kind != DateTimeKind.Utc)
            {
                dateTime = dateTime.ToUniversalTime();
            }

            return FromUniversalTime(index, dateTime);
        }

        /// <summary>
        /// Convert a time from UTC to the time zone with the supplied index.
        /// </summary>
        /// <param name="index">The time zone index.</param>
        /// <param name="utc">The time to convert.</param>
        /// <returns>The converted time.</returns>
        /// <exception cref="ArgumentOutOfRangeException">Thrown if the index is not found.</exception>
        internal static DateTime FromUniversalTime(int index, DateTime utc)
        {
            TimeZoneInformation tzi = FromIndex(index);

            if (timezoneSecurityExceptionOccured)
            {
                // Converting to SystemTime can cause security exceptions on systems not configured for
                // full trust on the MyComputer zone. In this case, approximate.
                return DateTime.SpecifyKind(utc.AddMinutes(tzi.Bias + tzi.StandardBias), DateTimeKind.Local);
            }

            else
            {
                try
                {
                    return tzi.FromUniversalTime(utc);
                }

                catch (SecurityException)
                {
                    timezoneSecurityExceptionOccured = true;
                    return FromUniversalTime(index, utc);
                }
            }
        }

        /// <summary>
        /// Converts the universal time.
        /// </summary>
        /// <param name="index">The timezone index</param>
        /// <param name="local">The local time</param>
        /// <returns></returns>
        internal static DateTime ToUniversalTime(int index, DateTime local)
        {
            TimeZoneInformation tzi = FromIndex(index);
            return tzi.ToUniversalTime(local);
        }

        #region IComparable<int> Members

        public int CompareTo(TimeZoneInformation other)
        {
            return displayName.CompareTo(other.displayName);
        }

        #endregion

        /// <summary>
        /// Get the currently selected time zone
        /// </summary>
        internal static TimeZoneInformation CurrentTimeZone
        {
            get
            {
                // The currently selected time zone information can
                // be retrieved using the Win32 GetTimeZoneInformation call,
                // but it only gives us names, offsets and dates - crucially,
                // not the Index.
                TIME_ZONE_INFORMATION tziNative;
                NativeMethods.GetTimeZoneInformation(out tziNative);

                // Getting the identity is tricky; the best we can do
                // is a match on the properties.
                foreach (TimeZoneInformation timeZoneInformation in TimeZones)
                {
                    if (timeZoneInformation.tzi.bias == tziNative.Bias &&
                        timeZoneInformation.tzi.daylightBias == tziNative.DaylightBias &&
                        timeZoneInformation.tzi.standardBias == tziNative.StandardBias &&
                        timeZoneInformation.standardName == tziNative.StandardName &&
                        timeZoneInformation.daylightName == tziNative.DaylightName)
                    {
                        return timeZoneInformation;
                    }
                }

                return null;
            }
        }

        /// <summary>
        /// Get a TimeZoneInformation for a supplied index.
        /// </summary>
        /// <param name="index">The time zone to find.</param>
        /// <returns>The corresponding TimeZoneInformation.</returns>
        /// <exception cref="System.ArgumentOutOfRangeException">Thrown if the index is not found.</exception>
        internal static TimeZoneInformation FromIndex(int index)
        {
            foreach (TimeZoneInformation timeZoneInformation in TimeZones)
            {
                if (timeZoneInformation.Index == index)
                    return timeZoneInformation;
            }

            throw new ArgumentOutOfRangeException("index", index, "TimeZoneInformation.FromIndex");
        }

        /// <summary>
        /// Enumerate the available time timeZones
        /// </summary>
        /// <returns>The list of known time timeZones</returns>
        internal static TimeZoneInformation[] TimeZones
        {
            get
            {
                if (timeZones == null)
                {
                    lock (lockTimeZones)
                    {
                        if (timeZones == null)
                        {
                            List<TimeZoneInformation> zoneList = new List<TimeZoneInformation>();

                            try
                            {
                                const string registryKey = @"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time Zones";

                                RegistryPermission permission =
                                    new RegistryPermission
                                        (RegistryPermissionAccess.Read, registryKey);

                                permission.Demand();

                                using (RegistryKey key = Registry.LocalMachine.OpenSubKey(registryKey))
                                {
                                    string[] zoneNames = key.GetSubKeyNames();

                                    foreach (string zoneName in zoneNames)
                                    {
                                        using (RegistryKey subKey = key.OpenSubKey(zoneName))
                                        {
                                            TimeZoneInformation tzi = new TimeZoneInformation();
                                            tzi.name = zoneName;
                                            tzi.displayName = (string) subKey.GetValue("Display");
                                            tzi.standardName = (string) subKey.GetValue("Std");
                                            tzi.daylightName = (string) subKey.GetValue("Dlt");
                                            tzi.index = (int) (subKey.GetValue("Index"));
                                            tzi.InitTzi((byte[]) subKey.GetValue("Tzi"));
                                            zoneList.Add(tzi);
                                        }
                                    }
                                }

                                timeZones = zoneList.ToArray();
                            }

                            catch (SecurityException)
                            {
                                timeZones = LoadFromResource();
                            }

                            Array.Sort(TimeZones);
                        }
                    }
                }

                return timeZones;
            }
        }

        /// <summary>
        /// Loads resources from stored resource
        /// </summary>
        /// <returns></returns>
        internal static TimeZoneInformation[] LoadFromResource()
        {
            XmlSerializer serializer = new XmlSerializer(typeof (TimeZoneInformation[]));

            using (StringReader reader = new StringReader(Resources.TimeZones))
            {
                return (TimeZoneInformation[]) serializer.Deserialize(reader);
            }
        }

        /// <summary>
        /// The zone's name.
        /// </summary>
        public string Name
        {
            get { return name; }
        }

        /// <summary>
        /// The zone's display name, e.g. '(GMT) Greenwich Mean Time : Dublin, Edinburgh, Lisbon, London'.
        /// </summary>
        public string DisplayName
        {
            get { return displayName; }
            set { displayName = value; }
        }

        /// <summary>
        /// The zone's index. No obvious pattern.
        /// </summary>
        public int Index
        {
            get { return index; }
            set { index = value; }
        }

        /// <summary>
        /// The zone's name during 'standard' time (not daylight savings).
        /// </summary>
        public string StandardName
        {
            get { return standardName; }
            set { standardName = value; }
        }

        /// <summary>
        /// The zone's name during daylight savings time.
        /// </summary>
        internal string DaylightName
        {
            get { return daylightName; }
            set { daylightName = value; }
        }

        internal TZI NativeTzi
        {
            get { return tzi; }
            set { tzi = value; }
        }

        public override string ToString()
        {
            return displayName;
        }

        /// <summary>
        /// The standard Windows SYSTEMTIME structure.
        /// </summary>
        [StructLayout(LayoutKind.Sequential)]
        [SuppressMessage("Microsoft.Naming", "CA1705:LongAcronymsShouldBePascalCased")]
        [SuppressMessage("Microsoft.Performance", "CA1815:OverrideEqualsAndOperatorEqualsOnValueTypes")]
        internal struct SYSTEMTIME
        {
            [SuppressMessage("Microsoft.Design", "CA1051:DoNotDeclareVisibleInstanceFields")] internal Int16 wYear;
            [SuppressMessage("Microsoft.Design", "CA1051:DoNotDeclareVisibleInstanceFields")] internal Int16 wMonth;
            [SuppressMessage("Microsoft.Design", "CA1051:DoNotDeclareVisibleInstanceFields")] internal Int16 wDayOfWeek;
            [SuppressMessage("Microsoft.Design", "CA1051:DoNotDeclareVisibleInstanceFields")] internal Int16 wDay;
            [SuppressMessage("Microsoft.Design", "CA1051:DoNotDeclareVisibleInstanceFields")] internal Int16 wHour;
            [SuppressMessage("Microsoft.Design", "CA1051:DoNotDeclareVisibleInstanceFields")] internal Int16 wMinute;
            [SuppressMessage("Microsoft.Design", "CA1051:DoNotDeclareVisibleInstanceFields")] internal Int16 wSecond;

            [SuppressMessage("Microsoft.Design", "CA1051:DoNotDeclareVisibleInstanceFields")] internal Int16
                wMilliseconds;
        }

        // FILETIME is already declared in System.Runtime.InteropServices.

        /// <summary>
        /// The layout of the Tzi xmlRpcValue in the registry.
        /// </summary>
        [StructLayout(LayoutKind.Sequential)]
        [SuppressMessage("Microsoft.Naming", "CA1705:LongAcronymsShouldBePascalCased")]
        internal struct TZI
        {
            public int bias;
            public int standardBias;
            public int daylightBias;
            public SYSTEMTIME standardDate;
            public SYSTEMTIME daylightDate;
        }

        /// <summary>
        /// The standard Win32 TIME_ZONE_INFORMATION structure.
        /// Thanks to www.pinvoke.net.
        /// </summary>
        [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
        struct TIME_ZONE_INFORMATION
        {
            [MarshalAs(UnmanagedType.I4)] public Int32 Bias;

            [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)] public string StandardName;

            public SYSTEMTIME StandardDate;

            [MarshalAs(UnmanagedType.I4)] public Int32 StandardBias;

            [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)] public string DaylightName;

            public SYSTEMTIME DaylightDate;

            [MarshalAs(UnmanagedType.I4)] public Int32 DaylightBias;
        }

        /// <summary>
        /// A container for P/Invoke declarations.
        /// </summary>
        struct NativeMethods
        {
            const string KERNEL32 = "kernel32.dll";

            [DllImport(KERNEL32)]
            internal static extern uint GetTimeZoneInformation(
                out TIME_ZONE_INFORMATION
                    lpTimeZoneInformation);

            [DllImport(KERNEL32)]
            [return : MarshalAs(UnmanagedType.Bool)]
            internal static extern bool SystemTimeToTzSpecificLocalTime(
                [In] ref TIME_ZONE_INFORMATION lpTimeZone,
                [In] ref SYSTEMTIME lpUniversalTime,
                out SYSTEMTIME lpLocalTime);

            [DllImport(KERNEL32)]
            [return : MarshalAs(UnmanagedType.Bool)]
            internal static extern bool SystemTimeToFileTime(
                [In] ref SYSTEMTIME lpSystemTime,
                out FILETIME lpFileTime);

            [DllImport(KERNEL32)]
            [return : MarshalAs(UnmanagedType.Bool)]
            internal static extern bool FileTimeToSystemTime(
                [In] ref FILETIME lpFileTime,
                out SYSTEMTIME lpSystemTime);

            /// <summary>
            /// Convert a local time to UTC, using the supplied time zone information.
            /// Windows XP and Server 2003 and later only.
            /// </summary>
            /// <param name="lpTimeZone">The time zone to use.</param>
            /// <param name="lpLocalTime">The local time to convert.</param>
            /// <param name="lpUniversalTime">The resultant time in UTC.</param>
            /// <returns>true if successful, false otherwise.</returns>
            [DllImport(KERNEL32)]
            [return : MarshalAs(UnmanagedType.Bool)]
            internal static extern bool TzSpecificLocalTimeToSystemTime(
                [In] ref TIME_ZONE_INFORMATION lpTimeZone,
                [In] ref SYSTEMTIME lpLocalTime,
                out SYSTEMTIME lpUniversalTime);
        }

        /// <summary>
        /// Initialise the tzi member.
        /// </summary>
        /// <param name="info">The Tzi data from the registry.</param>
        void InitTzi(byte[] info)
        {
            if (info.Length != Marshal.SizeOf(tzi))
            {
                throw new ArgumentException("Timezone Information length error");
            }

            // Could have sworn there's a Marshal operation to pack bytes into
            // a structure, but I can't see it. Do it manually.
            GCHandle handle = GCHandle.Alloc(info, GCHandleType.Pinned);

            try
            {
                tzi = (TZI) Marshal.PtrToStructure(handle.AddrOfPinnedObject(), typeof (TZI));
            }

            finally
            {
                handle.Free();
            }
        }

        /// <summary>
        /// The offset from UTC. Local = UTC + Bias.
        /// </summary>
        internal int Bias
        {
            // Biases in the registry are defined as UTC = local + bias
            // We return as Local = UTC + bias
            get { return -tzi.bias; }
        }

        /// <summary>
        /// The offset from UTC during standard time.
        /// </summary>
        internal int StandardBias
        {
            get { return -(tzi.bias + tzi.standardBias); }
        }

        /// <summary>
        /// The offset from UTC during daylight time.
        /// </summary>
        internal int DaylightBias
        {
            get { return -(tzi.bias + tzi.daylightBias); }
        }

        TIME_ZONE_INFORMATION TziNative()
        {
            TIME_ZONE_INFORMATION tziNative = new TIME_ZONE_INFORMATION();

            tziNative.Bias = tzi.bias;
            tziNative.StandardDate = tzi.standardDate;
            tziNative.StandardBias = tzi.standardBias;
            tziNative.DaylightDate = tzi.daylightDate;
            tziNative.DaylightBias = tzi.daylightBias;

            return tziNative;
        }

        /// <summary>
        /// Convert a time interpreted as UTC to a time in this time zone.
        /// </summary>
        /// <param name="utc">The UTC time to convert.</param>
        /// <returns>The corresponding local time in this zone.</returns>
        internal DateTime FromUniversalTime(DateTime utc)
        {
            // Convert to SYSTEMTIME
            SYSTEMTIME stUTC = DateTimeToSystemTime(utc);

            // Set up the TIME_ZONE_INFORMATION

            TIME_ZONE_INFORMATION tziNative = TziNative();
            SYSTEMTIME stLocal;
            NativeMethods.SystemTimeToTzSpecificLocalTime(ref tziNative, ref stUTC, out stLocal);

            // Convert back to DateTime
            return DateTime.SpecifyKind(SystemTimeToDateTime(ref stLocal), DateTimeKind.Local);
        }

        /// <summary>
        /// Convert a time interpreted as a local time in this zone to the equivalent UTC.
        /// Note that there may be different possible interpretations at the daylight
        /// time boundaries.
        /// </summary>
        /// <param name="local">The local time to convert.</param>
        /// <returns>The corresponding UTC.</returns>
        /// <exception cref="NotSupportedException">Thrown if the method failed due to missing platform support.</exception>
        internal DateTime ToUniversalTime(DateTime local)
        {
            SYSTEMTIME stLocal = DateTimeToSystemTime(local);
            TIME_ZONE_INFORMATION tziNative = TziNative();

            try
            {
                SYSTEMTIME stUTC;
                NativeMethods.TzSpecificLocalTimeToSystemTime(ref tziNative, ref stLocal, out stUTC);
                return SystemTimeToDateTime(ref stUTC);
            }

            catch (EntryPointNotFoundException e)
            {
                throw new NotSupportedException(e.Message);
            }
        }

        static SYSTEMTIME DateTimeToSystemTime(DateTime dt)
        {
            SYSTEMTIME st;
            FILETIME ft = new FILETIME();

            ft.dwHighDateTime = (int) (dt.Ticks >> 32);
            ft.dwLowDateTime = (int) (dt.Ticks & 0xFFFFFFFFL);
            NativeMethods.FileTimeToSystemTime(ref ft, out st);
            return st;
        }

        static DateTime SystemTimeToDateTime(ref SYSTEMTIME st)
        {
            FILETIME ft;
            NativeMethods.SystemTimeToFileTime(ref st, out ft);
            DateTime dt = new DateTime((((long) ft.dwHighDateTime) << 32) | (uint) ft.dwLowDateTime);
            return dt;
        }
    }
}